#!/usr/bin/env python
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""This script provides a suite of utility functions for the chroot.
"""

import optparse
import os
import shutil
import subprocess
import sys

_PROG = "crdev"
_SSH_KEY_FILEPATH = os.path.expanduser("~/.ssh/id_rsa")
_PUB_KEY_FILENAME = "~/.ssh/id_rsa.pub"
_PUB_KEY_FILEPATH = os.path.expanduser(_PUB_KEY_FILENAME)
_AUTH_KEYS_FILEPATH = os.path.expanduser("~/.ssh/authorized_keys")
_CHROME_MOUNT_PATH = "/tmp/chrome"
# Note: no ~/ for sshfs:
_DEFAULT_HOST_CHROOT_DIR = "chromeos/chroot"
_HOST_CHROOT_DIR_FILENAME = os.path.expanduser("~/.chromedir")

RAN_SUDO = False

###############################################################################
# Utility commands

def _PrintError(msg, err=""):
  """Print a message with an optional error message to stderr"""
  if err:
    print >> sys.stderr, "%s: %s" % (msg, err)
  else:
    print >> sys.stderr, msg

def _Confirm(message, default_response="n"):
  """Request confirmation and return True/False."""
  default_lc = default_response.lower()
  if default_lc == "y":
    input_msg = "%s [Y/n] " % message
  else:
    input_msg = "%s [y/N] " % message

  reply = raw_input(input_msg).lower()
  if reply:
    return reply == "y"
  return default_lc == "y"


def _CheckOverwriteFile(filepath):
  """Check for a file and request confirmation if it exists."""
  if os.path.isfile(filepath):
    if not _Confirm("%s already exists. Overwrite?" % filepath):
      return False
  return True


def _WriteFile(filepath, contents):
  """Write string |contents| to |filepath|."""
  try:
    print "Writing to: ", filepath
    with open(filepath,"w") as f_1:
      f_1.write(contents)
  except EnvironmentError as err:
    _PrintError("Failed to write to file '%s'" % filepath, err)
    return False

  return True


def _ReadFile(filepath):
  """Return contents of |filepath| as a string."""
  try:
    print "Reading from: " + filepath
    with open(filepath,"r") as f_1:
      result = f_1.read()
  except EnvironmentError as err:
    _PrintError("Failed to read from file '%s'" % filepath, err)
    return None

  return result.rstrip()


def _Sudo():
  """Request sudo access with message."""
  global RAN_SUDO
  if not RAN_SUDO:
    print "Executing: sudo -v"
  try:
    subprocess.call(["sudo", "-v"])
  except EnvironmentError as err:
    _PrintError("Failed to run sudo", err)
    return False
  RAN_SUDO = True
  return True


def _RunCommand(args):
  """Pass |args| to subprocess.call() and check the result."""
  cmd = ''.join([x + " " for x in args])
  try:
    if args[0] == "sudo":
      if _Sudo() == False:
        return False
    print "Executing: ", cmd
    retcode = subprocess.call(args)
  except EnvironmentError as err:
    _PrintError("Failed to run command '%s'" % cmd, err)
    return False
  else:
    if retcode != 0:
      _PrintError("Error running command '%s'" % cmd, "%s" % retcode)
      return False
    return True


def _GetCommandOutput(args):
  """Pass |args| to subprocess.Popen and return the output."""
  cmd = ''.join([x + " " for x in args])
  try:
    proc = subprocess.Popen(args, stdout=subprocess.PIPE)
    result = proc.communicate()[0]
  except EnvironmentError as err:
    _PrintError("Failed to run command '%s'" % cmd, err)
    return None
  result = result.rstrip('\n')
  return result


def _MakeDirectory(dirpath, warn=True):
  """Create directory |dirpath| if it does not exist."""
  if not os.path.isdir(dirpath):
    try:
      print "Creating directory: ", dirpath
      os.makedirs(dirpath)
    except EnvironmentError as err:
      if warn:
        _PrintError("Failed to create directory '%s'" % dirpath, err)
      return False
  return True


def _RemoveFile(filepath):
  """Remove file |filepath| if it exists."""
  if os.path.isfile(filepath):
    try:
      print "Removing file: ", filepath
      os.remove(filepath)
    except EnvironmentError as err:
      _PrintError("Failed to remove file '%s'" % filepath, err)
      return False
  return True


def _RemoveDirectory(dirpath):
  """Recursively remove directory |dirpath| if it exists."""
  if os.path.isdir(dirpath):
    try:
      print "Removing directory: ", dirpath
      shutil.rmtree(dirpath)
    except EnvironmentError as err:
      _PrintError("Failed to remove dir '%s'" % dirpath, err)
      return False
  return True


def _DevUser():
  """Extract the user name from lsb-release."""
  # TODO(stevenjb): Refactor this using python re.
  awk_expr  = """/CHROMEOS_RELEASE_DESCRIPTION/ {"""
  awk_expr += """ sub(/.*Build - /,"");"""
  awk_expr += """ sub(/\).*/,"");"""
  awk_expr += """ print; }"""
  return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"])


def _DevHost():
  """Extract the host name from lsb-release."""
  # TODO(stevenjb): Refactor this using python re.
  awk_expr  = """/CHROMEOS_DEVSERVER/ {"""
  awk_expr += """ sub(/.*http:\/\//,"");"""
  awk_expr += """ sub(/:8080.*/,"");"""
  awk_expr += """ print; }"""
  return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"])


def _DevBoard():
  """Extract the board from lsb-release."""
  # TODO(stevenjb): Refactor this using python re.
  awk_expr  = """/CHROMEOS_RELEASE_BOARD/ {"""
  awk_expr += """ sub(/.*=/,"");"""
  awk_expr += """ print; }"""
  return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"])


def _GetChrootDir(prompt_for_dir=False):
  """Get the name for the chrome directory on the host."""
  if os.path.isfile(_HOST_CHROOT_DIR_FILENAME):
    chromedir = _ReadFile(_HOST_CHROOT_DIR_FILENAME)
  else:
    chromedir = _DEFAULT_HOST_CHROOT_DIR

  if prompt_for_dir:
    host = _DevHost()
    prompt = ("Chroot directory on %s [ %s ]: " % (host, chromedir))
    inputdir = raw_input(prompt).rstrip()
    if inputdir:
      chromedir = inputdir
    _WriteFile(_HOST_CHROOT_DIR_FILENAME, chromedir)

  return chromedir


def _DaemonizeAndReRunAsRoot(cmd):
  """Double-fork to become owned by init, then re-exec this tool as root.

  Double-forking is the standard way to detach yourself from a
  process tree and become owned by init.  By doing this, you get to
  live on even if your parent goes away."""
  try:
    pid = os.fork()
    if pid > 0:
      sys.exit(0)  # Just want to go away silently.
  except OSError, e:
    return False
  try:
    pid = os.fork()
    if pid > 0:
      sys.exit(0)  # Just want to go away silently.
  except OSError, e:
    return False

  # re-run self as root.
  try:
    os.execlp('sudo', sys.executable, __file__, cmd)
  except OSError, e:
    _PrintError("Unable to exec self as root: %s", str(e))
    return False


###############################################################################
# Other Commands

def TestCommand(args):
  """Test command."""
  _WriteFile("/foo/test", "test")
  if len(args) == 0:
    args = ["sudo", "ls", "/"]
    return _RunCommand(args)
  return False


def GetBoard(unused_args=0):
  """Gets the board name from /etc/lsb-release."""
  print _DevBoard()
  return True


def GetUser(unused_args=0):
  """Gets the user name from /etc/lsb-release."""
  print _DevUser()
  return True


def GetHost(unused_args=0):
  """Gets the host name from /etc/lsb-release."""
  print _DevHost()
  return True


def MountWriteable(unused_args=0):
  """Remounts / as rw."""
  return _RunCommand(["sudo", "mount", "-o", "remount,rw", "/"])


def ShowIP(unused_args=0):
  """Shows the IP address of the device."""
  proc1 = subprocess.Popen(["/sbin/ifconfig", "eth0"], stdout=subprocess.PIPE)
  awk_cmd  = """/inet addr/ {"""
  awk_cmd += """ sub(/.*inet addr:/,""); sub(/  Bcast:.*/,"");"""
  awk_cmd += """ print; }"""
  proc2 = subprocess.Popen(
      ["awk", awk_cmd], stdin=proc1.stdout, stdout=subprocess.PIPE)
  proc1.stdout.close()
  result = proc2.communicate()[0].rstrip('\n')
  print "IP: ", result
  return True


def KillChrome(unused_args=0):
  """Kills all chrome processes and prevents restarting of chrome."""
  res = True
  res &= _RunCommand(["touch", "/var/run/disable_chrome_restart"])
  res &= _RunCommand(["sudo", "pkill", "-9", "chrome"])
  return res


def ClearOwnership(unused_args=0):
  """Clears any state related to the device Ownership (NOT TPM ownership)."""
  res = True
  res &= _RunCommand(["sudo", "stop", "ui"])

  whitelist_dir = "/var/lib/whitelist"
  for file in os.listdir(whitelist_dir):
    res &= _RemoveFile(os.path.join(whitelist_dir, file))

  res &= _RemoveDirectory("/home/chronos/Local State")
  res &= _RemoveFile("/home/chronos/Consent To Send Stats")
  res &= _RunCommand(["sudo", "start", "ui"])

  if not res:
    _PrintError("Errors encountered while clearing ownership state.")
    return False
  return True


def ShowOobe(unused_args=0):
  """Removes .oobe_completed and Local State directory."""
  res = True
  res &= _RemoveFile("/home/chronos/.oobe_completed")
  res &= ClearOwnership()

  if not res:
    _PrintError("Errors encountered while clearing setting up OOBE mode.")
    return False
  return _RunCommand(["sudo", "reboot"])


###############################################################################
# Setup Commands

def SetBash(unused_args):
  """Sets the default shell to bash."""
  if not _Sudo():
    return False
  if not MountWriteable():
    return False
  res = True
  res &= _RunCommand(["chsh", "-s", "/bin/bash"])
  res &= _RunCommand(["chsh", "-s", "/bin/bash", "chronos"])
  return res


def SetupBashrc(unused_args):
  """Sets up .bashrc."""
  filepath = os.path.expanduser("~/.bashrc")
  if not _CheckOverwriteFile(filepath):
    return True

  print "Writing to: ", filepath
  bashrc  = "#!/bin/bash\n"
  bashrc += "# .bashrc file set by %s\n" % _PROG
  bashrc += "export DISPLAY=:0.0\n"
  bashrc += "export PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin\n"
  bashrc += "/sbin/ifconfig eth0 | grep 'inet addr'\n"
  if not _WriteFile(filepath, bashrc):
    return False

  filepath = os.path.expanduser("~/.bash_profile")
  print "Writing to: ", filepath
  bashprofile  = "#!/bin/bash\n"
  bashprofile += ". $HOME/.bashrc\n"
  if not _WriteFile(filepath, bashprofile):
    return False
  return True


def SetupDev(unused_args):
  """Developer friendly setup: skip oobe, disable suspend,
\t\tenable ssh and remote debugging, run commands from
\t\t/tmp,/home."""
  if not _Sudo():
    return False
  if not MountWriteable():
    return False
  res = True
  res &= _WriteFile("/home/chronos/.oobe_completed", "1\n")
  res &= _MakeDirectory("/usr/share/power_manager")
  res &= _WriteFile("/tmp/disable_idle_suspend", "1\n")
  res &= _RunCommand(["sudo", "cp", "/tmp/disable_idle_suspend",
                      "/usr/share/power_manager/"])
  # Enable iptables and system-services for remote debugging
  for filename in ["iptables", "saft"]:
    res &= _RunCommand(["sudo", "sed", "-i", "-e",
                       "s/#for_test //", "/etc/init/%s.conf" % filename])
  # Allow commands to be run from /home and /tmp
  res &= _RunCommand(["sudo", "sed", "-i", "-e",
                     "s/#mod_for_test#//g", "/sbin/chromeos_startup"])
  return res


def SetupSsh(unused_args):
  """Sets up ssh configuration so that the dev host can ssh to the device."""
  if not MountWriteable():
    return False

  user = _DevUser()
  host = _DevHost()

  res = True
  if _CheckOverwriteFile(_SSH_KEY_FILEPATH):
    res &= _RemoveFile(_SSH_KEY_FILEPATH)
    # Generate an ssh key
    if _RunCommand(["ssh-keygen", "-f", _SSH_KEY_FILEPATH, "-N", "", "-q"]):
      host_source_path = "%s@%s:%s" % (user, host, _PUB_KEY_FILENAME)
      # Copy the ssh key to the host
      res &= _RunCommand(["scp", host_source_path, _AUTH_KEYS_FILEPATH])

  # Enable ssh to device
  res &= _RunCommand(
      ["sudo", "sed", "-i", "s/#for_test //", "/etc/init/openssh-server.conf"])

  return res


def AuthorizeSsh(unused_args):
  """Authorizes this netbook to connect to your dev host.
\t\t*Only use this on a device in a secure location!*"""

  user = _DevUser()
  host = _DevHost()

  if not os.path.isdir(os.path.expanduser("~/.ssh")):
    print "Run '%s ssh' to set up .ssh directory first." % _PROG
    return False

  if not _Confirm("This will append %s to authorized_keys on %s. "
                  "Are you sure?" % (_PUB_KEY_FILENAME, host)):
    return False

  proc1 = subprocess.Popen(["cat", _PUB_KEY_FILEPATH], stdout=subprocess.PIPE)
  try:
    ssh_args = ["ssh", user+"@"+host, "cat >> ~/.ssh/authorized_keys"]
    proc2 = subprocess.Popen(
        ssh_args, stdin=proc1.stdout, stdout=subprocess.PIPE)
  except EnvironmentError as err1:
    _PrintError("Error executing '%s'" % ' '.join(ssh_args), err1)
    return False
  try:
    proc1.stdout.close()
    result, err2 = proc2.communicate()
  except EnvironmentError:
    _PrintError("Error completing ssh command '%s'" % result, err2)
    return False
  return True


def SetupSshfsForChrome(unused_args):
  """<chrome-drir> Sets up sshfs mount to chrome directory on host."""

  user = _DevUser()
  host = _DevHost()

  chrootdir = _GetChrootDir(True)

  target = ("%s@%s:%s/var/lib/portage/distfiles-target/chrome-src"
            % (user, host, chrootdir))
  print "Setting up sshfs mount to: ", target

  res = True
  res &= _RunCommand(["sudo", "modprobe", "fuse"])
  if os.path.isdir(_CHROME_MOUNT_PATH):
    res &= _RunCommand(["fusermount", "-q", "-u", _CHROME_MOUNT_PATH])
    res &= _RemoveDirectory(_CHROME_MOUNT_PATH)
  res &= _MakeDirectory(_CHROME_MOUNT_PATH)
  res &= _RunCommand(["sshfs", target, _CHROME_MOUNT_PATH])
  res &= _RunCommand(["sudo", "/sbin/iptables",
                     "-A", "INPUT", "-p", "tcp", "--dport", "1234",
                     "-j", "ACCEPT"])
  return res

###############################################################################
# Multi-commands (convenience functions)

def Setup(args):
  """Performs default developer setup (bash,bashrc,dev,ssh)."""
  if not SetBash(args):
    if not _Confirm("Bash setup failed. Continue?", "y"):
      return False
  if not SetupBashrc(args):
    if not _Confirm(".bashrc setup failed. Continue?", "y"):
      return False
  if not SetupDev(args):
    if not _Confirm("Dev setup failed. Continue?", "y"):
      return False
  if not SetupSsh(args):
    return False
  return True

###############################################################################

_SETUP_COMMANDS = {
    'setup': Setup,
    'dev': SetupDev,
    'bash': SetBash,
    'bashrc': SetupBashrc,
    'ssh': SetupSsh,
    'sshauthorize': AuthorizeSsh,
    'sshfs': SetupSshfsForChrome,
}


_OTHER_COMMANDS = {
    'mountrw': MountWriteable,
    'ip': ShowIP,
    'test': TestCommand,
    'board': GetBoard,
    'user': GetUser,
    'host': GetHost,
}


_CLEANUP_COMMANDS = {
    'show_oobe': ShowOobe,
    'clear_owner': ClearOwnership,
}


def GetUsage(commands):
  """Get the docstring for each command."""
  usage = ""
  for cmd in commands.items():
    usage += " "
    usage += cmd[0]
    usage += ":\t"
    if len(cmd[0]) < 6:
      usage += "\t"
    doc = cmd[1].__doc__
    if doc:
      usage += doc
    usage += "\n"
  return usage


###############################################################################

def main():
  """Main crdev function"""
  usage = """usage: crdev command [options]

Note: Beta! Feature requests / changes can be sent to:
  stevenjb@chromium.org (for now)

"""

  usage += "Setup Commands:\n"
  usage += GetUsage(_SETUP_COMMANDS)
  usage += "Other Commands:\n"
  usage += GetUsage(_OTHER_COMMANDS)
  usage += "Cleanup Commands:\n"
  usage += GetUsage(_CLEANUP_COMMANDS)

  parser = optparse.OptionParser(usage)
  args = parser.parse_args()[1]

  if not args:
    print usage
    return

  cmd = args[0]
  root_ok = cmd in _CLEANUP_COMMANDS.keys()
  if not root_ok or os.geteuid() != 0:
    if os.getenv('USER') != 'chronos':
      _PrintError("%s must be run as chronos." % _PROG)
      return

  if cmd in _SETUP_COMMANDS.keys():
    res = _SETUP_COMMANDS[cmd](args[1:])
  elif cmd in _OTHER_COMMANDS.keys():
    res = _OTHER_COMMANDS[cmd](args[1:])
  elif cmd in _CLEANUP_COMMANDS.keys():
    if os.geteuid() != 0:
      _DaemonizeAndReRunAsRoot(cmd)
      _PrintError("Should never return from DaemonizeAndReRunAsRoot()")
      return
    res = _CLEANUP_COMMANDS[cmd](args[1:])
  else:
    parser.error("Unknown command: " + cmd)
    return

  if not res:
    _PrintError("Errors encountered when running '%s'" % ' '.join(args))
  else:
    print "Success!"


if __name__ == '__main__':
  main()
